﻿/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.

Copyright (C) 2005-2014  Sylvain Rougeaux (Cyber Sinh) (http://www.luminescence-software.org/)
*/

using System;
using System.Collections.Generic;
using System.IO;

namespace Luminescence.Xiph
{
   /// <summary>
   ///   Classe de lecture / écriture des tags des fichiers Ogg Vorbis, FLAC (.ogg) et Speex (.spx).
   /// </summary>
   public sealed class OggTagger : VorbisComment
   {
      // Membres génériques
      private readonly List<int> pagesToRemove = new List<int>();
      private byte[] vorbisSetupHeader;

      // Membres spécifiques FLAC
      private readonly List<byte[]> metadataBlocks = new List<byte[]>(4);

      // Membres spécifiques Speex
      private bool speexVBR;

      // Identificateurs
      private static readonly byte[] vorbis1 = { 0x01, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73 };
      private static readonly byte[] vorbis3 = { 0x03, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73 };
      private static readonly byte[] fLaC = { 0x66, 0x4C, 0x61, 0x43 };
      private static readonly byte[] Speex___ = { 0x53, 0x70, 0x65, 0x65, 0x78, 0x20, 0x20, 0x20 };

      /// <summary>
      ///   Constructeur de la classe OggTagger.
      /// </summary>
      /// <param name = "fileName">Chemin d'accès du fichier Ogg à analyser</param>
      public OggTagger(string fileName)
      {
         ReadMetadata(fileName);
      }

      /// <summary>
      ///   Lit les métadonnées du fichier Ogg.
      /// </summary>
      /// <param name = "fileName">Chemin d'accès du fichier Ogg à analyser</param>
      public override void ReadMetadata(string fileName)
      {
         FileName = fileName;
         ClearTags();
         ClearArts();
         metadataBlocks.Clear();

         using (var fs = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read))
         {
            Length = fs.Length;

            if (Length == 0)
               throw new FileFormatException("The file is not a valid Ogg file because it is empty.");

            var page = new OggPageReader(fs);
            byte[] firstSegment = page.GetSegments()[0];

            if (firstSegment.ArrayEquals(vorbis1, 0, vorbis1.Length))
               ReadVorbis(page, firstSegment);
            else if (firstSegment.ArrayEquals(fLaC, 9, fLaC.Length))
               ReadFlac(page, firstSegment);
            else if (firstSegment.ArrayEquals(Speex___, 0, Speex___.Length))
               ReadSpeex(page, firstSegment);
            else
               throw new FileFormatException("The file is not a valid Ogg Vorbis, Ogg FLAC or Speex file");

            if (page.MultiplexedStreamDetected)
               throw new FileFormatException("The file contains multiplexed streams.");
         }
      }

      private void ReadSpeex(OggPageReader page, byte[] firstSegment)
      {
         Codec = OggCodec.Speex;

         using (var ms = new MemoryStream(firstSegment))
         {
            var br = new BinaryReader(ms);
            ms.Position = 36;

            SampleRate = br.ReadInt32();
            SpeexBand = (SpeexBand)br.ReadInt32();

            ms.Position += 4;
            Channels = (byte)br.ReadInt32();
            VorbisSpeexNominalBitrate = br.ReadInt32();
            ms.Position += 4;
            speexVBR = (br.ReadInt32() == 1);
         }

         using (var ms = new MemoryStream())
         {
            do
            {
               page.ReadNextPage();
               pagesToRemove.Add(page.PageSequenceNumber);
               byte[] data = page.GetData();
               ms.Write(data, 0, data.Length);
            } while (!page.IsComplete);

            ms.Position = 0;
            ReadVorbisComment(ms, false);
         }

         page.ReadLastPage();
         Samples = page.AbsoluteGranulePostion;
      }

      private void ReadFlac(OggPageReader page, byte[] firstSegment)
      {
         Codec = OggCodec.FLAC;
         byte[] buffer = null;

         using (var ms = new MemoryStream(firstSegment))
         {
            var br = new BinaryReader(ms);

            ms.Position = 27;
            buffer = br.ReadBytes(8);

            SampleRate = buffer[0];
            SampleRate = (SampleRate << 8) | buffer[1];
            SampleRate = (SampleRate << 4) | (buffer[2] >> 4);

            Channels = (byte)(((buffer[2] >> 1) & 7) + 1);

            FlacBitsPerSample = (byte)((((buffer[2] & 1) << 4) | (buffer[3] >> 4)) + 1);

            Samples = (buffer[3] & 15);
            Samples = (Samples << 8) | buffer[4];
            Samples = (Samples << 8) | buffer[5];
            Samples = (Samples << 8) | buffer[6];
            Samples = (Samples << 8) | buffer[7];
         }

         bool last_metadata_block;
         do
         {
            using (var ms = new MemoryStream())
            {
               var br = new BinaryReader(ms);
               do
               {
                  page.ReadNextPage();
                  pagesToRemove.Add(page.PageSequenceNumber);
                  byte[] data = page.GetData();
                  ms.Write(data, 0, data.Length);
               } while (!page.IsComplete);

               ms.Position = 0;
               byte startHeader = br.ReadByte();
               last_metadata_block = (startHeader >> 7) == 1;
               var blockType = (FlacMetadataBlock)(startHeader & 127);

               buffer = br.ReadBytes(3);
               int size = buffer[0];
               size = (size << 8) | buffer[1];
               size = (size << 8) | buffer[2];

               switch (blockType)
               {
                  case FlacMetadataBlock.StreamInfo:
                     throw new FileFormatException("The file contains several Stream Info blocks.");
                  case FlacMetadataBlock.Padding:
                     ms.Position += size;
                     break;
                  case FlacMetadataBlock.VorbisComment:
                     ReadVorbisComment(ms, false);
                     break;
                  case FlacMetadataBlock.Picture:
                     buffer = br.ReadBytes(size);
                     AddArt(new ID3PictureFrame(buffer));
                     break;
                  default: // APPLICATION, SEEKTABLE, CUESHEET, other blocks
                     var metadata = new byte[size + 4];
                     metadata[0] = (byte)blockType;
                     buffer.CopyTo(metadata, 1);
                     buffer = br.ReadBytes(size);
                     buffer.CopyTo(metadata, 4);
                     metadataBlocks.Add(metadata);
                     break;
               }
            }
         } while (!last_metadata_block);
      }

      private void ReadVorbis(OggPageReader page, byte[] firstSegment)
      {
         Codec = OggCodec.Vorbis;

         using (var ms = new MemoryStream(firstSegment))
         {
            var br = new BinaryReader(ms);

            ms.Position = 11;
            Channels = br.ReadByte();
            SampleRate = br.ReadInt32();
            MaximumVorbisBitrate = br.ReadInt32();
            VorbisSpeexNominalBitrate = br.ReadInt32();
            MinimumVorbisBitrate = br.ReadInt32();
         }

         using (var ms = new MemoryStream())
         {
            do
            {
               page.ReadNextPage();
               pagesToRemove.Add(page.PageSequenceNumber);
               byte[] data = page.GetData();
               ms.Write(data, 0, data.Length);
            } while (!page.IsComplete);

            if (!ms.ToArray().ArrayEquals(vorbis3, 0, vorbis3.Length))
               throw new FileFormatException("The Vorbis comment header cannot be found in the file.");

            ms.Position = 7;
            ReadVorbisComment(ms, true);

            while (true) // go to vorbis setup header if it is on the same page of comments and save it
            {
               int id = ms.ReadByte();
               if (id == -1) break;
               if (id == 5)
               {
                  ms.Position--;
                  var dataSizeToKeep = (int)(ms.Length - ms.Position);
                  vorbisSetupHeader = new byte[dataSizeToKeep];
                  ms.Read(vorbisSetupHeader, 0, dataSizeToKeep);
                  break;
               }
            }
         }

         page.ReadLastPage();
         Samples = page.AbsoluteGranulePostion;
      }

      /// <summary>
      ///   Enregistrement les tags dans le fichier Ogg.
      /// </summary>
      public override void SaveMetadata()
      {
         string tempFilename = FileName + ".tmp";
         FileStream fsSource = null;
         FileStream fsDest = null;
         try
         {
            fsSource = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
            fsDest = new FileStream(tempFilename, FileMode.Create, FileAccess.ReadWrite, FileShare.None);

            var page = new OggPageReader(fsSource);
            var newPages = new List<OggPage>();
            int number = 0;

            byte[] data = page.ToArray();
            fsDest.Write(data, 0, data.Length);

            switch (Codec)
            {
               case OggCodec.Vorbis:
                  byte[] vorbisComments = WriteVorbisComment(true, true);
                  data = new byte[vorbisComments.Length + vorbis3.Length];
                  vorbis3.CopyTo(data, 0);
                  vorbisComments.CopyTo(data, vorbis3.Length);
                  newPages.AddRange(OggPage.CreateFromPaquet(data, page.StreamSerialNumber));
                  if (vorbisSetupHeader != null)
                     newPages.AddRange(OggPage.CreateFromPaquet(vorbisSetupHeader, page.StreamSerialNumber));
                  break;

               case OggCodec.FLAC:
                  int nbMetadataBlocks = Arts.Count + metadataBlocks.Count;

                  byte[] flacComments = FlacTagger.CreateBlock(WriteVorbisComment(false, false), FlacMetadataBlock.VorbisComment, nbMetadataBlocks <= 0);
                  newPages.AddRange(OggPage.CreateFromPaquet(flacComments, page.StreamSerialNumber));

                  foreach (ID3PictureFrame picture in Arts)
                  {
                     nbMetadataBlocks--;
                     byte[] pictureFrame = FlacTagger.CreateBlock(picture.ToApicFrame(), FlacMetadataBlock.Picture, nbMetadataBlocks <= 0);
                     newPages.AddRange(OggPage.CreateFromPaquet(pictureFrame, page.StreamSerialNumber));
                  }

                  foreach (byte[] metadataBlock in metadataBlocks)
                  {
                     nbMetadataBlocks--;
                     if (nbMetadataBlocks <= 0)
                        metadataBlock[0] = (byte)(metadataBlock[0] | 128); // Identifier when it is the last metadata block (first bit = 1)
                     newPages.AddRange(OggPage.CreateFromPaquet(metadataBlock, page.StreamSerialNumber));
                  }
                  break;

               case OggCodec.Speex:
                  byte[] speexComments = WriteVorbisComment(true, false);
                  newPages.AddRange(OggPage.CreateFromPaquet(speexComments, page.StreamSerialNumber));
                  break;
            }

            foreach (OggPage newPage in newPages)
            {
               newPage.Renumber(++number);
               data = newPage.ToArray();
               fsDest.Write(data, 0, data.Length);
            }

            while (page.ReadNextPage())
            {
               if (!pagesToRemove.Contains(page.PageSequenceNumber))
               {
                  page.Renumber(++number);
                  data = page.ToArray();
                  fsDest.Write(data, 0, data.Length);
               }
            }

            fsSource.Dispose();
            fsDest.Dispose();
            File.SetAttributes(FileName, FileAttributes.Normal);
            File.Replace(tempFilename, FileName, null, true);
         }
         catch
         {
            fsSource?.Dispose();
            if (fsDest != null)
            {
               fsDest.Dispose();
               if (File.Exists(tempFilename))
                  File.Delete(tempFilename);
            }

            throw;
         }
      }

      #region Informations techniques

      /// <summary>
      ///   Retourne le type de codec utilisé dans le fichier Ogg.
      /// </summary>
      public OggCodec Codec { get; private set; }

      /// <summary>
      ///   Retourne le type de largeur de bande utilisé dans le fichier Ogg Speex.
      /// </summary>
      public SpeexBand SpeexBand { get; private set; }

      /// <summary>
      ///   Retourne le chemin et le nom du fichier Ogg analysé.
      /// </summary>
      public string FileName { get; private set; }

      /// <summary>
      ///   Retourne le nombre de canaux dans le fcihier Ogg.
      /// </summary>
      public byte Channels { get; private set; }

      /// <summary>
      ///   Retourne le bitrate minimum du fichier Ogg Vorbis.
      /// </summary>
      public int MinimumVorbisBitrate { get; private set; }

      /// <summary>
      ///   Retourne le taux d'échantillonnage du fichier Ogg.
      /// </summary>
      public int SampleRate { get; private set; }

      /// <summary>
      ///   Retourne le bitrate maximum du fichier Ogg Vorbis.
      /// </summary>
      public int MaximumVorbisBitrate { get; private set; }

      /// <summary>
      ///   Retourne le bitrate nominal du fichier Ogg Vorbis ou Speex.
      /// </summary>
      public int VorbisSpeexNominalBitrate { get; private set; }

      /// <summary>
      ///   Retourne Vrai si le fichier Ogg Vorbis ou Speex est encodé avec un débit variable.
      /// </summary>
      public bool IsVariableBitrate
      {
         get
         {
            switch (Codec)
            {
               case OggCodec.Vorbis:
                  return ((MaximumVorbisBitrate != MinimumVorbisBitrate) || (MinimumVorbisBitrate != VorbisSpeexNominalBitrate));
               case OggCodec.Speex:
                  return speexVBR;
               default:
                  return false;
            }
         }
      }

      /// <summary>
      ///   Retourne le niveau de qualité du fichier Ogg Vorbis (de 0.0 à 1.0).
      /// </summary>
      public float VorbisQuality
      {
         get
         {
            switch ((int)Math.Round((double)(VorbisSpeexNominalBitrate / 1000), 0))
            {
               case 64:
                  return 0F;
               case 72:
                  return 0.05F;
               case 80:
                  return 0.1F;
               case 88:
                  return 0.15F;
               case 96:
                  return 0.2F;
               case 104:
                  return 0.25F;
               case 112:
                  return 0.3F;
               case 120:
                  return 0.35F;
               case 128:
                  return 0.4F;
               case 144:
                  return 0.45F;
               case 160:
                  return 0.5F;
               case 176:
                  return 0.55F;
               case 192:
                  return 0.6F;
               case 208:
                  return 0.65F;
               case 224:
                  return 0.7F;
               case 240:
                  return 0.75F;
               case 256:
                  return 0.8F;
               case 288:
                  return 0.85F;
               case 320:
                  return 0.9F;
               case 410:
                  return 0.95F;
               case 500:
                  return 1F;
               default:
                  return -1;
            }
         }
      }

      /// <summary>
      ///   Retourne la version du codec utilisé.
      /// </summary>
      public string CodecVersion
      {
         get
         {
            switch (Codec)
            {
               case OggCodec.Vorbis:
                  return VorbisVendor.Vendors.ContainsKey(Vendor) ? VorbisVendor.Vendors[Vendor] : Vendor;

               case OggCodec.FLAC:
                  string[] flacBuffer = Vendor.Split(new[] { ' ' });
                  return flacBuffer.Length > 2 ? flacBuffer[2] : Vendor;

               case OggCodec.Speex:
                  string[] speexBuffer = Vendor.Split(new[] { '-' });
                  return speexBuffer.Length > 1 ? speexBuffer[1] : Vendor;

               default:
                  return null;
            }
         }
      }

      /// <summary>
      ///   Retourne la taille du fichiers Ogg (en octets).
      /// </summary>
      public long Length { get; private set; }

      /// <summary>
      ///   Retourne le nombre d'échantillon contenu dans le fichier Ogg.
      /// </summary>
      public long Samples { get; private set; }

      /// <summary>
      ///   Retourne la durée du fichier Ogg (en secondes).
      /// </summary>
      public float Duration => (float)Math.Round((double)Samples / SampleRate, 3);

      /// <summary>
      ///   Retourne le bitrate moyen du fichier Ogg.
      /// </summary>
      public int AverageBitrate => (int)Math.Round((double)Length * 8 / Duration, 0);

      /// <summary>
      ///   Retourne le taux de compression du ficher Ogg.
      /// </summary>
      public float Ratio => (float)Length / UncompressedFileLength;

      /// <summary>
      ///   Retourne la taille de l'échantillon du fichier Ogg FLAC.
      /// </summary>
      public byte FlacBitsPerSample { get; private set; }

      /// <summary>
      ///   Retourne la taille du fichier décompressée (en octets).
      /// </summary>
      public long UncompressedFileLength
      {
         get
         {
            byte bitsPerSample = Codec == OggCodec.FLAC ? FlacBitsPerSample : (byte)16;
            return (long)Math.Round(((double)(Duration * SampleRate * bitsPerSample * Channels / 8) + 44), 0);
         }
      }

      #endregion
   }
}